Skip to content

addons: Reduce memory consumption#2395

Merged
danmar merged 8 commits into
cppcheck-opensource:masterfrom
jubnzv:addons-reduce-mem-usage
Dec 27, 2019
Merged

addons: Reduce memory consumption#2395
danmar merged 8 commits into
cppcheck-opensource:masterfrom
jubnzv:addons-reduce-mem-usage

Conversation

@jubnzv

@jubnzv jubnzv commented Nov 23, 2019

Copy link
Copy Markdown
Contributor

Parse dump files incrementaly using ElementTree.iterparse. Clean unused resources during parsing. This method is explained in the following article: https://www.ibm.com/developerworks/xml/library/x-hiperfparse/

Memory consumption was reduced about 30% (measured with mprof), execution time increased about 5% (measured with time utility).

Resulting graphs created with mprof:

master

Picture

this PR

Picture

Python version: 2.7.15.

I used 20 Mb dump file created from this source.

Parse dump files incrementaly using ElementTree.iterparse. Clean unused
resources during parsing.  This method is explained in following
article: https://www.ibm.com/developerworks/xml/library/x-hiperfparse/

Memory consumption was reduced about 30% (measured with mprof),
execution time increased about 5% (measured with time utility).
More description available in PR.
Comment thread addons/cppcheckdata.py
Comment thread addons/cppcheckdata.py Outdated
@versat

versat commented Nov 24, 2019

Copy link
Copy Markdown
Collaborator

This really looks very promising. Good find!

Use lxml module instead default xml.etree. Lxml provides convenient
wrappers around iterparse method that accepts `tag` argument. That
easer incremental parsing routines to select specific tags from roottree
like `dump` and `dumps`.

Element.clear() method was replaced by `lxml_clean` because lxml
keeps additional information to nodes that should be removed.

Added note about large consumption RAM on large dump files.
This commit doesn't solve this problem completely, but provides a way
to improve current parser to add incremental Configuration serialization
later.
@jubnzv

jubnzv commented Nov 24, 2019

Copy link
Copy Markdown
Contributor Author

In the linked article they also clear the references to node (they call it elem):

# Also eliminate now-empty references from the root node to <Title>
while elem.getprevious() is not None:
    del elem.getparent()[0]

Would that make sense here too? I really don't know.

No, that makes not sense before, because we was use standard xml.etree.ElementTree instead lxml as suggested in the article.

As I understand, lxml is somehow extends ElementTree class from standard library. In particular, it adds links to parent nodes in childs. So we want to remove this links from cleared nodes during iteration, to tell gc free unused resources.

Now I added lxml module as suggested in the article because their iterparse method provides argument tag. It allows search nodes with given tag for iteration without traverse the whole tree. This sufficiently reduce execution time with the same dumpfile:

Picture

It is because we don't iterate over all unnecessary nodes as here. And it faster than current master implementation, when we iterate whole tree twice.

So, can we add lxml as dependency to cppcheck? It's is quite a popular module and I'm sure that most *nix users already have it pre-installed.

@jubnzv

jubnzv commented Nov 24, 2019

Copy link
Copy Markdown
Contributor Author

These changes reduce execution time and memory consumption in comparison with master. All tests will work if lxml would be added.

But in fact, this does not solve the problem with RAM completely. What will happen if we try to check a really large dump file? Consider 430+ Mb dump of jcdctmgr.c:

git clone https://github.com/mozilla/mozjpeg && cd mozjpeg
$INSTALL_PATH/bin/cppcheck -j4 --enable=all . --dump
du -h jcdctmgr.c.dump  # 431M

If we run misra.py with memory-profiler (mprof run misra.py jaricom.c.dump), we'll see something like this:

Picture

About 8 Gb RAM when loading variables and tokens during Configuration serialization.

What should we do about this problem? May be... check the size of XML dump before loading and, if it's requires more RAM than available, use iterparse in Configuration constructor? I suppose it will be very slow (need to check), but it doesn't kill the process at least.

Any suggestions?

@versat

versat commented Nov 24, 2019

Copy link
Copy Markdown
Collaborator

I see. I did not have in mind that lxml is not used.
Maybe the memory consumption reduces to something acceptable when the constructor calls are delayed, like suggested in #2329 (Configuration for example).

IMHO it would be the best if users do not have to install extra python modules for using addons. That would make it more complicated to use the addons than it is now. Personally I would only use available modules for the addons. But I can not decide this.
What do you think about it @danmar

@jubnzv

jubnzv commented Nov 24, 2019

Copy link
Copy Markdown
Contributor Author

Maybe the memory consumption reduces to something acceptable when the constructor calls are delayed

Unfortunately, no. If we have no enough memory to load whole XML in memory, we'll die at this place. You can check it out on the large dump file from my post above.

IMHO it would be the best if users do not have to install extra python modules for using addons.

I agree, it can complicate installation process. Ok, I'll try to find other ways to do iterparse without lxml.

@danmar

danmar commented Nov 24, 2019

Copy link
Copy Markdown
Collaborator

I agree, it can complicate installation process. Ok, I'll try to find other ways to do iterparse without lxml.

It is not a requirement to use xml.. feel free to suggest some alternative.

@danmar

danmar commented Nov 25, 2019

Copy link
Copy Markdown
Collaborator

I think this sounds like a major refactoring that it's better to merge after the release.

@danmar danmar added the merge-after-next-release Wait with merging this PR until after the next Release label Nov 25, 2019
@versat

versat commented Nov 25, 2019

Copy link
Copy Markdown
Collaborator

Maybe the memory consumption reduces to something acceptable when the constructor calls are delayed

Unfortunately, no. If we have no enough memory to load whole XML in memory, we'll die at this place. You can check it out on the large dump file from my post above.

When using iterparse we do not need to load the whole XML into memory, so this additional optimization could help to further reduce the memory consumption I thought.
It seems like there are many ways to optimize the scripts and we need to implement several of them.
IMO using iterparse, like implemented in the first commit, was fine. That could be the first out of several steps optimizing the scripts.

@danmar

danmar commented Nov 25, 2019

Copy link
Copy Markdown
Collaborator

IMO using iterparse, like implemented in the first commit, was fine. That could be the first out of several steps optimizing the scripts.

sounds ok to me.

@jubnzv

jubnzv commented Nov 26, 2019

Copy link
Copy Markdown
Contributor Author

@versat you was absolutely right to mention Configuration objects.

I rewrote iterative parser correctly (without third-party modules, correct resources cleanup) and, sigh, it wasn't solve this problem. With this parser we'll save only dump_file_size amount of RAM. This is quite insufficient in comparison of overall memory consumption. In particular, it saves 20 (of 400) Mb RAM with jaricom.c dump and ~400 (of ~10000) Mb with jcdctmgr.c dump.

So I was wondering what's really requires so much memory. Obliviously, the problem in serialized Python objects. So I use convenient pympler module to measure memory usage of particular objects. Here's some results with jaricom.c dump:

Append configuration 31954 Kb. New overall size: 0 Kb
Append configuration 31966 Kb. New overall size: 30024 Kb
Append configuration 31962 Kb. New overall size: 60048 Kb
Append configuration 31968 Kb. New overall size: 90046 Kb
Append configuration 31965 Kb. New overall size: 120072 Kb
Append configuration 32478 Kb. New overall size: 150065 Kb
Append configuration 32477 Kb. New overall size: 180610 Kb
Append configuration 31948 Kb. New overall size: 211140 Kb
Append configuration 31945 Kb. New overall size: 241142 Kb
Append configuration 31946 Kb. New overall size: 271144 Kb
Append configuration 31949 Kb. New overall size: 301145 Kb
Append configuration 31999 Kb. New overall size: 331142 Kb

Overall size: 361159 Kb
        All rawtokens: 1550 (5261 Kb)
        All tokens: 80192     (340218 Kb)
        All variables: 6230   (169743 Kb)
        All functions: 1156   (98174 Kb)
        All directives: 3972  (1978 Kb)

As we see, the most memory consumed by configurations list. So the solution seems pretty straightforward. If we see, that our dump files is pretty large, we should return iterator to configurations. The values for particular configuration would be parsed by demand.

The size of metadata from <dumps> nodes is insignificant. We could parse it in Configuration.__init__. And in constructor we should detect which way we will for parsing configurations: serialize all objects in memory (faster) or use iterative approach (slower, more resource-friendly). I suppose it could be done based on available amount of virtual memory.

Further experiments will take some time, I'll continue work on it.

P.S. Actually, I'm not interested at all in mozjpeg project. It was just random github repo, which crashed my small script which scraps github repos and try to use Cppcheck addons on them.

@versat

versat commented Nov 26, 2019

Copy link
Copy Markdown
Collaborator

Very interesting, thanks for sharing these finds.
Did you test with Python 2 or Python 3, or even both? My experience is that Python 3 is faster and uses significantly less memory. So I am just curious about from which version these numbers come.
I do not expect that Python 3 solves any of the problems with the huge memory consumption, but it should at least use less memory.

I am not sure if it makes sense to implement different approaches for parsing the Configuration.
I fear that could have some drawbacks (like harder readability and maintainability, more error-prone), and I am also not sure if it is possible to tell which approach is the best by looking at the free virtual memory or so. But I could be wrong and this works great in the end :)

@jubnzv

jubnzv commented Nov 26, 2019

Copy link
Copy Markdown
Contributor Author

Did you test with Python 2 or Python 3, or even both?

I'm still using python 2.7.15 for all experiments to make sure that we have backward compatibility. Undoubtedly using latest python 3 versions will improve performance and optimize resources usage. Especially useful for us would be dataclasses (introduced in python 3.7). It would be interesting to explore other mechanisms like __slots__ and... may there is something else? This is a good place for further optimizations.

I'm not yet sure about configurations parsing, need to try that first and write tests later.

@jubnzv

jubnzv commented Nov 27, 2019

Copy link
Copy Markdown
Contributor Author

I finished iterative parser for Configuration objects. I got a significant reduction in memory consumption with same execution time in comparison with current master version.

Here are my results:

master python2

Picture

master python3

Picture

PR python2

Picture

PR python3

Picture

Environment: Debian 10.2, Python 3.7.3 and 2.7.16. Target machine: i5-3230M / 16 GiB DDR3-1600 RAM.

I use dump file jaricom.c described in previous posts. The similar results can be obtained with any dump file, e.g. addons test files or cppcheck sources.

@danmar @versat could you please review these changes? I also want to squash my commits to write a good commit message before merging if you accept it.

@jubnzv

jubnzv commented Nov 27, 2019

Copy link
Copy Markdown
Contributor Author

After these changes it also possible to process the huge dumps. Just look at these graphs given from handling jcdctmgr.c.dump:

python2

Picture

python3

Picture

I can't reproduce same with master, because I have no enough memory on my machine.

Comment thread addons/findcasts.py Outdated
Comment thread addons/threadsafety.py Outdated
@versat

versat commented Nov 28, 2019

Copy link
Copy Markdown
Collaborator

I have not looked too deeply into the code but from what I have seen, it looks fine.
Also, some tests I made revealed no issues. So I would give it a go as soon as 1.90 has been released.

@danmar

danmar commented Nov 30, 2019

Copy link
Copy Markdown
Collaborator

So I would give it a go as soon as 1.90 has been released.

That is OK!

@jubnzv jubnzv mentioned this pull request Nov 30, 2019
@jubnzv

jubnzv commented Dec 21, 2019

Copy link
Copy Markdown
Contributor Author

@danmar I suppose it can be merged now.

@versat versat requested a review from danmar December 25, 2019 09:48
@danmar

danmar commented Dec 27, 2019

Copy link
Copy Markdown
Collaborator

Let's try it!

@danmar danmar merged commit d977761 into cppcheck-opensource:master Dec 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-after-next-release Wait with merging this PR until after the next Release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants